﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics;

namespace Qreed.Windows.Forms
{
    /// <summary>
    /// CanvasControl maintains a page size, may be zoomed to any level large or small and may be scrolled as you would expect. Furthermore, Canvas backtracks the mouse input so that the position of the mouse on the screen is translated to the exact virtual coordinates of the mouse in the page.
    /// </summary>
    /// <remarks>
    /// From http://www.bobpowell.net/canvas.aspx
    /// </remarks>
    public class CanvasControl : ScrollableControl
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="CanvasControl"/> class.
        /// </summary>
        public CanvasControl()
        {
            this.SetStyle(ControlStyles.AllPaintingInWmPaint |
                          ControlStyles.UserPaint |
                          ControlStyles.DoubleBuffer |
                          ControlStyles.ResizeRedraw, true);
        }

        private Size _pageSize = new Size(640, 480);
        /// <summary>
        /// Gets or sets the size of the page.
        /// </summary>
        /// <value>The size of the page.</value>
        [Category("Appearance")]
        [Description("The size of the virtual page")]
        public Size PageSize
        {
            get { return _pageSize; }
            set
            {
                _pageSize = value;
                OnPageSizeChanged(EventArgs.Empty);
            }
        }

        private bool _doubleBuffer = true;
        /// <summary>
        /// Gets or sets a value indicating whether to enable double buffering.
        /// </summary>
        /// <value><c>true</c> if double bufferering is enabled; otherwise, <c>false</c>.</value>
        [Category("Behavior")]
        [Description("Set true to enable double buffering")]
        [DefaultValue(true)]
        public bool DoubleBuffer
        {
            get { return _doubleBuffer; }
            set
            {
                _doubleBuffer = value;

                if (value)
                    SetStyle(ControlStyles.DoubleBuffer, true);
                else
                    SetStyle(ControlStyles.DoubleBuffer, false);

                Invalidate();
            }
        }

        private Color _pageColor = Color.White;
        /// <summary>
        /// Gets or sets the color of the page.
        /// </summary>
        /// <value>The color of the page.</value>
        [Category("Appearance")]
        [Description("The base color of the page")]
        [DefaultValue(typeof(Color), "White")]
        public Color PageColor
        {
            get { return _pageColor; }
            set
            {
                _pageColor = value;
                Invalidate();
            }
        }

        private bool _clipToPage = true;
        /// <summary>
        /// Gets or sets a value indicating whether to clip to the page.
        /// </summary>
        /// <value>When <c>true</c> no drawing is allowed outside page boundaries; otherwise, <c>false</c>.</value>
        [Category("Behavior")]
        [Description("Gets or sets the clipping flag. When true no drawing is allowed outside page boundaries")]
        [DefaultValue(true)]
        public bool ClipToPage
        {

            get { return _clipToPage; }
            set
            {
                _clipToPage = value;
                Invalidate();
            }
        }

        private float _zoom = 1.0f;
        /// <summary>
        /// Gets or sets the zoom multiplyer factor.
        /// </summary>
        /// <value>The zoom factor.</value>
        [Category("Behavior")]
        [Description("The multiplying factor for the zoom level")]
        [DefaultValue(1.0f)]
        public float Zoom
        {
            get { return _zoom; }
            set
            {
                _zoom = value;

                //A zoom may not be negative

                if (_zoom < 0)
                    _zoom = Math.Abs(_zoom);

                //a zoom may be very small but never zero.
                if (_zoom == 0)
                    _zoom = 0.000001f;

                //and the host application code informed if needs be
                OnZoomChanged(EventArgs.Empty);
            }
        }

        /// <summary>
        /// Raises the <see cref="E:PageSizeChanged"/> event.
        /// </summary>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        protected virtual void OnPageSizeChanged(EventArgs e)
        {
            CalculateScrollMinSize();

            if (PageSizeChanged != null)
                PageSizeChanged(this, e);
        }

        /// <summary>
        /// Raises the <see cref="E:ZoomChanged"/> event.
        /// </summary>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        protected virtual void OnZoomChanged(EventArgs e)
        {
            CalculateScrollMinSize();

            if (ZoomChanged != null)
                ZoomChanged(this, e);

            Invalidate();
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Forms.Control.SizeChanged"/> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
        protected override void OnSizeChanged(EventArgs e)
        {
            CalculateScrollMinSize();

            base.OnSizeChanged(e);
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Forms.Control.Paint"/> event.
        /// </summary>
        /// <param name="e">A <see cref="T:System.Windows.Forms.PaintEventArgs"/> that contains the event data.</param>
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaintBackground(e);

            Matrix mx = GetTransformMatrix();

            e.Graphics.Transform = mx;

            SolidBrush b = new SolidBrush(Color.FromArgb(64, Color.Black));
            e.Graphics.FillRectangle(b, new Rectangle(new Point(20, 20), PageSize));
            b.Color = PageColor;
            e.Graphics.FillRectangle(b, new Rectangle(new Point(0, 0), PageSize));

            if (ClipToPage)
                e.Graphics.SetClip(new Rectangle(0, 0, PageSize.Width, PageSize.Height));

            base.OnPaint(e);
        }

        public virtual PointF CanvasPointToClient(PointF pt)
        {
            Matrix mx = GetTransformMatrix();

            PointF[] px = new PointF[] { pt };
            mx.TransformPoints(px);

            return px[0];
        }

        /// <summary>
        /// Transforms a client location to the location on the canvas.
        /// </summary>
        /// <param name="pt">The point to transform.</param>
        /// <returns>The transformed point.</returns>
        public virtual PointF ClientPointToCanvas(PointF pt)
        {
            Matrix mx = GetTransformMatrix();
            mx.Invert();

            PointF[] px = new PointF[] { pt };

            mx.TransformPoints(px);

            return px[0];
        }

        /// <summary>
        /// Transforms a client location to the location on the canvas.
        /// </summary>
        /// <param name="pt">The point to transform.</param>
        /// <returns>The transformed point.</returns>
        public Point ClientPointToCanvas(Point pt)
        {
            PointF ptf = ClientPointToCanvas((PointF)pt);
            
            return new Point((int)ptf.X, (int)ptf.Y);
        }

        /// <summary>
        /// Gets the transform matrix.
        /// </summary>
        /// <returns>A matrix which contains the current zoom and pan levels.</returns>
        public virtual Matrix GetTransformMatrix()
        {
            Matrix mx = new Matrix(Zoom, 0, 0, Zoom, 0, 0);

            float oneOverZoom = 1.0f / Zoom;

            // The size of the page, used to calculate which if any of the scrollbars should be seen and their 
            // offsets must be modified to take the zoom level into account. For this adjustment, the reciprocal of the zoom is used.

            Size s = new Size((int)(this.ClientSize.Width * oneOverZoom),
                              (int)(this.ClientSize.Height * oneOverZoom));

            if (s.Width > PageSize.Width)
                mx.Translate((s.Width / 2) - (_pageSize.Width / 2), 0);
            else
                mx.Translate((float)this.AutoScrollPosition.X, 0);

            if (s.Height > PageSize.Height)
                mx.Translate(0, (s.Height / 2) - (this._pageSize.Height / 2) + (this.AutoScrollPosition.Y));
            else
                mx.Translate(0, (float)this.AutoScrollPosition.Y);

            return mx;
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Forms.ScrollableControl.Scroll"/> event.
        /// </summary>
        /// <param name="se">A <see cref="T:System.Windows.Forms.ScrollEventArgs"/> that contains the event data.</param>
        protected override void OnScroll(ScrollEventArgs se)
        {
            base.OnScroll(se);

            Invalidate();
        }

        /// <summary>
        /// Occurs when the <see cref="PageSize"/> property has changed.
        /// </summary>
        public event EventHandler PageSizeChanged;

        /// <summary>
        /// Occurs when <see cref="Zoom"/> property has changed.
        /// </summary>
        public event EventHandler ZoomChanged;

        /// <summary>
        /// Calculates the size of the scroll min.
        /// </summary>
        /// <remarks>
        /// ScrollableControl enables us to set the minimum scroll size for the page. If the window becomes smaller than this size, scrollbars will appear and the page may be scrolled to it's maximum extents.
        /// </remarks>
        private void CalculateScrollMinSize()
        {
            SizeF cs = new SizeF(this.PageSize.Width * Zoom, this.PageSize.Height * Zoom);
            this.AutoScrollMinSize = cs.ToSize();
            Invalidate();
        }
    }
}
